// Copyright (c) 2017 Uber Technologies, Inc.
//

import moment, { unitOfTime } from 'moment'
import _dropWhile from 'lodash/dropWhile'
import _round from 'lodash/round'

import { toFloatPrecision } from '../views/dashboard/plugins/built-in/panel/trace/utils/number'

const TODAY = 'Today'
const YESTERDAY = 'Yesterday'

export const STANDARD_DATE_FORMAT = 'YYYY-MM-DD'
export const STANDARD_TIME_FORMAT = 'HH:mm'
export const STANDARD_DATETIME_FORMAT = 'MMMM D YYYY, HH:mm:ss.SSS'
export const ONE_MILLISECOND = 1000
export const ONE_SECOND = 1000 * ONE_MILLISECOND
export const ONE_MINUTE = 60 * ONE_SECOND
export const ONE_HOUR = 60 * ONE_MINUTE
export const ONE_DAY = 24 * ONE_HOUR
export const DEFAULT_MS_PRECISION = Math.log10(ONE_MILLISECOND)

const UNIT_STEPS: { unit: string; microseconds: number; ofPrevious: number }[] =
  [
    { unit: 'd', microseconds: ONE_DAY, ofPrevious: 24 },
    { unit: 'h', microseconds: ONE_HOUR, ofPrevious: 60 },
    { unit: 'm', microseconds: ONE_MINUTE, ofPrevious: 60 },
    { unit: 's', microseconds: ONE_SECOND, ofPrevious: 1000 },
    { unit: 'ms', microseconds: ONE_MILLISECOND, ofPrevious: 1000 },
    { unit: 'μs', microseconds: 1, ofPrevious: 1000 },
  ]

const timeUnitToShortTermMapper = {
  milliseconds: 'ms',
  seconds: 's',
  minutes: 'm',
  hours: 'h',
  days: 'd',
}

/**
 * @param {number} timestamp
 * @param {number} initialTimestamp
 * @param {number} totalDuration
 * @return {number} 0-100 percentage
 */
export function getPercentageOfDuration(
  duration: number,
  totalDuration: number,
) {
  return (duration / totalDuration) * 100
}

const quantizeDuration = (
  duration: number,
  floatPrecision: number,
  conversionFactor: number,
) =>
  toFloatPrecision(duration / conversionFactor, floatPrecision) *
  conversionFactor

/**
 * @param {number} duration (in microseconds)
 * @return {string} formatted, unit-labelled string with time in milliseconds
 */
export function formatDate(duration: number) {
  return moment(duration / ONE_MILLISECOND).format(STANDARD_DATE_FORMAT)
}

/**
 * @param {number} duration (in microseconds)
 * @return {string} formatted, unit-labelled string with time in milliseconds
 */
export function formatTime(duration: number) {
  return moment(duration / ONE_MILLISECOND).format(STANDARD_TIME_FORMAT)
}

/**
 * @param {number} duration (in microseconds)
 * @return {string} formatted, unit-labelled string with time in milliseconds
 */
export function formatDatetime(duration: number) {
  return moment(duration / ONE_MILLISECOND).format(STANDARD_DATETIME_FORMAT)
}

/**
 * @param {number} duration (in microseconds)
 * @return {string} formatted, unit-labelled string with time in milliseconds
 */
export function formatMillisecondTime(duration: number) {
  const targetDuration = quantizeDuration(
    duration,
    DEFAULT_MS_PRECISION,
    ONE_MILLISECOND,
  )
  return `${moment
    .duration(targetDuration / ONE_MILLISECOND)
    .asMilliseconds()}ms`
}

/**
 * @param {number} duration (in microseconds)
 * @return {string} formatted, unit-labelled string with time in seconds
 */
export function formatSecondTime(duration: number) {
  const targetDuration = quantizeDuration(
    duration,
    DEFAULT_MS_PRECISION,
    ONE_SECOND,
  )
  return `${moment.duration(targetDuration / ONE_MILLISECOND).asSeconds()}s`
}

/**
 * Humanizes the duration for display.
 *
 * Example:
 * 5000ms => 5s
 * 1000μs => 1ms
 * 183840s => 2d 3h
 *
 * @param {number} duration (in microseconds)
 * @return {string} formatted duration
 */
export function formatDuration(duration: number): string {
  // Drop all units that are too large except the last one
  const [primaryUnit, secondaryUnit] = _dropWhile(
    UNIT_STEPS,
    ({ microseconds }, index) =>
      index < UNIT_STEPS.length - 1 && microseconds > duration,
  )

  if (primaryUnit.ofPrevious === 1000) {
    // If the unit is decimal based, display as a decimal
    return `${_round(duration / primaryUnit.microseconds, 2)}${
      primaryUnit.unit
    }`
  }

  const primaryValue = Math.floor(duration / primaryUnit.microseconds)
  const primaryUnitString = `${primaryValue}${primaryUnit.unit}`
  const secondaryValue = Math.round(
    (duration / secondaryUnit.microseconds) % primaryUnit.ofPrevious,
  )
  const secondaryUnitString = `${secondaryValue}${secondaryUnit.unit}`
  return secondaryValue === 0
    ? primaryUnitString
    : `${primaryUnitString} ${secondaryUnitString}`
}

export function durationToSeconds(timeExpr) {
  var units = { h: 3600, m: 60, s: 1 }
  var regex = /(\d+)([hms])/g

  let seconds = 0
  var match
  while ((match = regex.exec(timeExpr))) {
    seconds += parseInt(match[1]) * units[match[2]]
  }

  return seconds
}

export function durationToMilliseconds(timeExpr) {
  var units = { h: 3600000, m: 60000, s: 1000, ms: 1 }
  var regex = /(\d+)(ms|[hms])/g

  let seconds = 0
  var match
  while ((match = regex.exec(timeExpr))) {
    seconds += parseInt(match[1]) * units[match[2]]
  }

  return seconds
}

export function formatRelativeDate(value: any, fullMonthName: boolean = false) {
  const m = moment.isMoment(value) ? value : moment(value)
  const monthFormat = fullMonthName ? 'MMMM' : 'MMM'
  const dt = new Date()
  if (dt.getFullYear() !== m.year()) {
    return m.format(`${monthFormat} D, YYYY`)
  }
  const mMonth = m.month()
  const mDate = m.date()
  const date = dt.getDate()
  if (mMonth === dt.getMonth() && mDate === date) {
    return TODAY
  }
  dt.setDate(date - 1)
  if (mMonth === dt.getMonth() && mDate === dt.getDate()) {
    return YESTERDAY
  }
  return m.format(`${monthFormat} D`)
}

export const getSuitableTimeUnit = (microseconds: number): string => {
  if (microseconds < 1000) {
    return 'microseconds'
  }

  const duration = moment.duration(microseconds / 1000, 'ms')

  return Object.keys(timeUnitToShortTermMapper)
    .reverse()
    .find((timeUnit) => {
      const durationInTimeUnit = duration.as(timeUnit as unitOfTime.Base)

      return durationInTimeUnit >= 1
    })!
}

export function convertTimeUnitToShortTerm(timeUnit: string) {
  if (timeUnit === 'microseconds') return 'μs'

  const shortTimeUnit = (timeUnitToShortTermMapper as any)[timeUnit]

  if (shortTimeUnit) return shortTimeUnit

  return ''
}

export function convertToTimeUnit(
  microseconds: number,
  targetTimeUnit: string,
) {
  if (microseconds < 1000) {
    return microseconds
  }

  return moment
    .duration(microseconds / 1000, 'ms')
    .as(targetTimeUnit as unitOfTime.Base)
}

export function timeConversion(microseconds: number) {
  if (microseconds < 1000) {
    return `${microseconds}μs`
  }

  const timeUnit = getSuitableTimeUnit(microseconds)

  return `${moment
    .duration(microseconds / 1000, 'ms')
    .as(timeUnit as unitOfTime.Base)}${convertTimeUnitToShortTerm(timeUnit)}`
}
